/* *
 *
 *  (c) 2009-2019 Øystein Moseng
 *
 *  Instrument class for sonification module.
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "../../parts/Globals.js";
import U from "../../parts/Utilities.js";
var pick = U.pick;
/**
 * A set of options for the Instrument class.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.InstrumentOptionsObject
 */ /**
 * The type of instrument. Currently only `oscillator` is supported. Defaults
 * to `oscillator`.
 * @name Highcharts.InstrumentOptionsObject#type
 * @type {string|undefined}
 */ /**
 * The unique ID of the instrument. Generated if not supplied.
 * @name Highcharts.InstrumentOptionsObject#id
 * @type {string|undefined}
 */ /**
 * When using functions to determine frequency or other parameters during
 * playback, this options specifies how often to call the callback functions.
 * Number given in milliseconds. Defaults to 20.
 * @name Highcharts.InstrumentOptionsObject#playCallbackInterval
 * @type {number|undefined}
 */ /**
 * A list of allowed frequencies for this instrument. If trying to play a
 * frequency not on this list, the closest frequency will be used. Set to `null`
 * to allow all frequencies to be used. Defaults to `null`.
 * @name Highcharts.InstrumentOptionsObject#allowedFrequencies
 * @type {Array<number>|undefined}
 */ /**
 * Options specific to oscillator instruments.
 * @name Highcharts.InstrumentOptionsObject#oscillator
 * @type {Highcharts.OscillatorOptionsObject|undefined}
 */
/**
 * Options for playing an instrument.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.InstrumentPlayOptionsObject
 */ /**
 * The frequency of the note to play. Can be a fixed number, or a function. The
 * function receives one argument: the relative time of the note playing (0
 * being the start, and 1 being the end of the note). It should return the
 * frequency number for each point in time. The poll interval of this function
 * is specified by the Instrument.playCallbackInterval option.
 * @name Highcharts.InstrumentPlayOptionsObject#frequency
 * @type {number|Function}
 */ /**
 * The duration of the note in milliseconds.
 * @name Highcharts.InstrumentPlayOptionsObject#duration
 * @type {number}
 */ /**
 * The minimum frequency to allow. If the instrument has a set of allowed
 * frequencies, the closest frequency is used by default. Use this option to
 * stop too low frequencies from being used.
 * @name Highcharts.InstrumentPlayOptionsObject#minFrequency
 * @type {number|undefined}
 */ /**
 * The maximum frequency to allow. If the instrument has a set of allowed
 * frequencies, the closest frequency is used by default. Use this option to
 * stop too high frequencies from being used.
 * @name Highcharts.InstrumentPlayOptionsObject#maxFrequency
 * @type {number|undefined}
 */ /**
 * The volume of the instrument. Can be a fixed number between 0 and 1, or a
 * function. The function receives one argument: the relative time of the note
 * playing (0 being the start, and 1 being the end of the note). It should
 * return the volume for each point in time. The poll interval of this function
 * is specified by the Instrument.playCallbackInterval option. Defaults to 1.
 * @name Highcharts.InstrumentPlayOptionsObject#volume
 * @type {number|Function|undefined}
 */ /**
 * The panning of the instrument. Can be a fixed number between -1 and 1, or a
 * function. The function receives one argument: the relative time of the note
 * playing (0 being the start, and 1 being the end of the note). It should
 * return the panning value for each point in time. The poll interval of this
 * function is specified by the Instrument.playCallbackInterval option.
 * Defaults to 0.
 * @name Highcharts.InstrumentPlayOptionsObject#pan
 * @type {number|Function|undefined}
 */ /**
 * Callback function to be called when the play is completed.
 * @name Highcharts.InstrumentPlayOptionsObject#onEnd
 * @type {Function|undefined}
 */
/**
 * @requires module:modules/sonification
 *
 * @interface Highcharts.OscillatorOptionsObject
 */ /**
 * The waveform shape to use for oscillator instruments. Defaults to `sine`.
 * @name Highcharts.OscillatorOptionsObject#waveformShape
 * @type {string|undefined}
 */
// Default options for Instrument constructor
var defaultOptions = {
  type: "oscillator",
  playCallbackInterval: 20,
  oscillator: {
    waveformShape: "sine",
  },
};
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
 * The Instrument class. Instrument objects represent an instrument capable of
 * playing a certain pitch for a specified duration.
 *
 * @sample highcharts/sonification/instrument/
 *         Using Instruments directly
 * @sample highcharts/sonification/instrument-advanced/
 *         Using callbacks for instrument parameters
 *
 * @requires module:modules/sonification
 *
 * @class
 * @name Highcharts.Instrument
 *
 * @param {Highcharts.InstrumentOptionsObject} options
 *        Options for the instrument instance.
 */
function Instrument(options) {
  this.init(options);
}
Instrument.prototype.init = function (options) {
  if (!this.initAudioContext()) {
    H.error(29);
    return;
  }
  this.options = H.merge(defaultOptions, options);
  this.id = this.options.id = (options && options.id) || H.uniqueKey();
  // Init the audio nodes
  var ctx = H.audioContext;
  this.gainNode = ctx.createGain();
  this.setGain(0);
  this.panNode = ctx.createStereoPanner && ctx.createStereoPanner();
  if (this.panNode) {
    this.setPan(0);
    this.gainNode.connect(this.panNode);
    this.panNode.connect(ctx.destination);
  } else {
    this.gainNode.connect(ctx.destination);
  }
  // Oscillator initialization
  if (this.options.type === "oscillator") {
    this.initOscillator(this.options.oscillator);
  }
  // Init timer list
  this.playCallbackTimers = [];
};
/**
 * Return a copy of an instrument. Only one instrument instance can play at a
 * time, so use this to get a new copy of the instrument that can play alongside
 * it. The new instrument copy will receive a new ID unless one is supplied in
 * options.
 *
 * @function Highcharts.Instrument#copy
 *
 * @param {Highcharts.InstrumentOptionsObject} [options]
 *        Options to merge in for the copy.
 *
 * @return {Highcharts.Instrument}
 *         A new Instrument instance with the same options.
 */
Instrument.prototype.copy = function (options) {
  return new Instrument(H.merge(this.options, { id: null }, options));
};
/**
 * Init the audio context, if we do not have one.
 * @private
 * @return {boolean} True if successful, false if not.
 */
Instrument.prototype.initAudioContext = function () {
  var Context = H.win.AudioContext || H.win.webkitAudioContext,
    hasOldContext = !!H.audioContext;
  if (Context) {
    H.audioContext = H.audioContext || new Context();
    if (
      !hasOldContext &&
      H.audioContext &&
      H.audioContext.state === "running"
    ) {
      H.audioContext.suspend(); // Pause until we need it
    }
    return !!(
      H.audioContext &&
      H.audioContext.createOscillator &&
      H.audioContext.createGain
    );
  }
  return false;
};
/**
 * Init an oscillator instrument.
 * @private
 * @param {Highcharts.OscillatorOptionsObject} oscillatorOptions
 * The oscillator options passed to Highcharts.Instrument#init.
 * @return {void}
 */
Instrument.prototype.initOscillator = function (options) {
  var ctx = H.audioContext;
  this.oscillator = ctx.createOscillator();
  this.oscillator.type = options.waveformShape;
  this.oscillator.connect(this.gainNode);
  this.oscillatorStarted = false;
};
/**
 * Set pan position.
 * @private
 * @param {number} panValue
 * The pan position to set for the instrument.
 * @return {void}
 */
Instrument.prototype.setPan = function (panValue) {
  if (this.panNode) {
    this.panNode.pan.setValueAtTime(panValue, H.audioContext.currentTime);
  }
};
/**
 * Set gain level. A maximum of 1.2 is allowed before we emit a warning. The
 * actual volume is not set above this level regardless of input.
 * @private
 * @param {number} gainValue
 * The gain level to set for the instrument.
 * @param {number} [rampTime=0]
 * Gradually change the gain level, time given in milliseconds.
 * @return {void}
 */
Instrument.prototype.setGain = function (gainValue, rampTime) {
  if (this.gainNode) {
    if (gainValue > 1.2) {
      console.warn(
        // eslint-disable-line
        "Highcharts sonification warning: " +
          "Volume of instrument set too high."
      );
      gainValue = 1.2;
    }
    if (rampTime) {
      this.gainNode.gain.setValueAtTime(
        this.gainNode.gain.value,
        H.audioContext.currentTime
      );
      this.gainNode.gain.linearRampToValueAtTime(
        gainValue,
        H.audioContext.currentTime + rampTime / 1000
      );
    } else {
      this.gainNode.gain.setValueAtTime(gainValue, H.audioContext.currentTime);
    }
  }
};
/**
 * Cancel ongoing gain ramps.
 * @private
 * @return {void}
 */
Instrument.prototype.cancelGainRamp = function () {
  if (this.gainNode) {
    this.gainNode.gain.cancelScheduledValues(0);
  }
};
/**
 * Get the closest valid frequency for this instrument.
 * @private
 * @param {number} frequency - The target frequency.
 * @param {number} [min] - Minimum frequency to return.
 * @param {number} [max] - Maximum frequency to return.
 * @return {number} The closest valid frequency to the input frequency.
 */
Instrument.prototype.getValidFrequency = function (frequency, min, max) {
  var validFrequencies = this.options.allowedFrequencies,
    maximum = pick(max, Infinity),
    minimum = pick(min, -Infinity);
  return !validFrequencies || !validFrequencies.length
    ? // No valid frequencies for this instrument, return the target
      frequency
    : // Use the valid frequencies and return the closest match
      validFrequencies.reduce(function (acc, cur) {
        // Find the closest allowed value
        return Math.abs(cur - frequency) < Math.abs(acc - frequency) &&
          cur < maximum &&
          cur > minimum
          ? cur
          : acc;
      }, Infinity);
};
/**
 * Clear existing play callback timers.
 * @private
 * @return {void}
 */
Instrument.prototype.clearPlayCallbackTimers = function () {
  this.playCallbackTimers.forEach(function (timer) {
    clearInterval(timer);
  });
  this.playCallbackTimers = [];
};
/**
 * Set the current frequency being played by the instrument. The closest valid
 * frequency between the frequency limits is used.
 * @param {number} frequency
 * The frequency to set.
 * @param {Highcharts.Dictionary<number>} [frequencyLimits]
 * Object with maxFrequency and minFrequency
 * @return {void}
 */
Instrument.prototype.setFrequency = function (frequency, frequencyLimits) {
  var limits = frequencyLimits || {},
    validFrequency = this.getValidFrequency(frequency, limits.min, limits.max);
  if (this.options.type === "oscillator") {
    this.oscillatorPlay(validFrequency);
  }
};
/**
 * Play oscillator instrument.
 * @private
 * @param {number} frequency - The frequency to play.
 */
Instrument.prototype.oscillatorPlay = function (frequency) {
  if (!this.oscillatorStarted) {
    this.oscillator.start();
    this.oscillatorStarted = true;
  }
  this.oscillator.frequency.setValueAtTime(
    frequency,
    H.audioContext.currentTime
  );
};
/**
 * Prepare instrument before playing. Resumes the audio context and starts the
 * oscillator.
 * @private
 */
Instrument.prototype.preparePlay = function () {
  this.setGain(0.001);
  if (H.audioContext.state === "suspended") {
    H.audioContext.resume();
  }
  if (this.oscillator && !this.oscillatorStarted) {
    this.oscillator.start();
    this.oscillatorStarted = true;
  }
};
/**
 * Play the instrument according to options.
 *
 * @sample highcharts/sonification/instrument/
 *         Using Instruments directly
 * @sample highcharts/sonification/instrument-advanced/
 *         Using callbacks for instrument parameters
 *
 * @function Highcharts.Instrument#play
 *
 * @param {Highcharts.InstrumentPlayOptionsObject} options
 *        Options for the playback of the instrument.
 *
 * @return {void}
 */
Instrument.prototype.play = function (options) {
  var instrument = this,
    duration = options.duration || 0,
    // Set a value, or if it is a function, set it continously as a timer.
    // Pass in the value/function to set, the setter function, and any
    // additional data to pass through to the setter function.
    setOrStartTimer = function (value, setter, setterData) {
      var target = options.duration,
        currentDurationIx = 0,
        callbackInterval = instrument.options.playCallbackInterval;
      if (typeof value === "function") {
        var timer = setInterval(function () {
          currentDurationIx++;
          var curTime = (currentDurationIx * callbackInterval) / target;
          if (curTime >= 1) {
            instrument[setter](value(1), setterData);
            clearInterval(timer);
          } else {
            instrument[setter](value(curTime), setterData);
          }
        }, callbackInterval);
        instrument.playCallbackTimers.push(timer);
      } else {
        instrument[setter](value, setterData);
      }
    };
  if (!instrument.id) {
    // No audio support - do nothing
    return;
  }
  // If the AudioContext is suspended we have to resume it before playing
  if (
    H.audioContext.state === "suspended" ||
    (this.oscillator && !this.oscillatorStarted)
  ) {
    instrument.preparePlay();
    // Try again in 10ms
    setTimeout(function () {
      instrument.play(options);
    }, 10);
    return;
  }
  // Clear any existing play timers
  if (instrument.playCallbackTimers.length) {
    instrument.clearPlayCallbackTimers();
  }
  // Clear any gain ramps
  instrument.cancelGainRamp();
  // Clear stop oscillator timer
  if (instrument.stopOscillatorTimeout) {
    clearTimeout(instrument.stopOscillatorTimeout);
    delete instrument.stopOscillatorTimeout;
  }
  // If a note is playing right now, clear the stop timeout, and call the
  // callback.
  if (instrument.stopTimeout) {
    clearTimeout(instrument.stopTimeout);
    delete instrument.stopTimeout;
    if (instrument.stopCallback) {
      // We have a callback for the play we are interrupting. We do not
      // allow this callback to start a new play, because that leads to
      // chaos. We pass in 'cancelled' to indicate that this note did not
      // finish, but still stopped.
      instrument._play = instrument.play;
      instrument.play = function () {};
      instrument.stopCallback("cancelled");
      instrument.play = instrument._play;
    }
  }
  // Stop the note without fadeOut if the duration is too short to hear the
  // note otherwise.
  var immediate = duration < H.sonification.fadeOutDuration + 20;
  // Stop the instrument after the duration of the note
  instrument.stopCallback = options.onEnd;
  var onStop = function () {
    delete instrument.stopTimeout;
    instrument.stop(immediate);
  };
  if (duration) {
    instrument.stopTimeout = setTimeout(
      onStop,
      immediate ? duration : duration - H.sonification.fadeOutDuration
    );
    // Play the note
    setOrStartTimer(options.frequency, "setFrequency", {
      minFrequency: options.minFrequency,
      maxFrequency: options.maxFrequency,
    });
    // Set the volume and panning
    setOrStartTimer(pick(options.volume, 1), "setGain", 4); // Slight ramp
    setOrStartTimer(pick(options.pan, 0), "setPan");
  } else {
    // No note duration, so just stop immediately
    onStop();
  }
};
/**
 * Mute an instrument that is playing. If the instrument is not currently
 * playing, this function does nothing.
 *
 * @function Highcharts.Instrument#mute
 */
Instrument.prototype.mute = function () {
  this.setGain(0.0001, H.sonification.fadeOutDuration * 0.8);
};
/**
 * Stop the instrument playing.
 *
 * @function Highcharts.Instrument#stop
 *
 * @param {boolean} immediately
 *        Whether to do the stop immediately or fade out.
 *
 * @param {Function} [onStopped]
 *        Callback function to be called when the stop is completed.
 *
 * @param {*} [callbackData]
 *        Data to send to the onEnd callback functions.
 *
 * @return {void}
 */
Instrument.prototype.stop = function (immediately, onStopped, callbackData) {
  var instr = this,
    reset = function () {
      // Remove timeout reference
      if (instr.stopOscillatorTimeout) {
        delete instr.stopOscillatorTimeout;
      }
      // The oscillator may have stopped in the meantime here, so allow
      // this function to fail if so.
      try {
        instr.oscillator.stop();
      } catch (e) {
        // silent error
      }
      instr.oscillator.disconnect(instr.gainNode);
      // We need a new oscillator in order to restart it
      instr.initOscillator(instr.options.oscillator);
      // Done stopping, call the callback from the stop
      if (onStopped) {
        onStopped(callbackData);
      }
      // Call the callback for the play we finished
      if (instr.stopCallback) {
        instr.stopCallback(callbackData);
      }
    };
  // Clear any existing timers
  if (instr.playCallbackTimers.length) {
    instr.clearPlayCallbackTimers();
  }
  if (instr.stopTimeout) {
    clearTimeout(instr.stopTimeout);
  }
  if (immediately) {
    instr.setGain(0);
    reset();
  } else {
    instr.mute();
    // Stop the oscillator after the mute fade-out has finished
    instr.stopOscillatorTimeout = setTimeout(
      reset,
      H.sonification.fadeOutDuration + 100
    );
  }
};
export default Instrument;
